/*
 *      Copyright 2008 Battams, Derek
 *       
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 */
package com.google.code.gwtsrwc.client;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.user.client.ui.Widget;

/**
 * A simple model used to represent the contents of a set of tabular data; each cell of the table is represented by a TableData object; TabelModels can be used as the basis for LiveFlexTable instances
 * @author dbattams
 * @version $Id: TableModel.java 25 2008-09-09 22:53:35Z derek@battams.ca $
 * @param <E> The type of objects uses to represent cells in the table; each object must implement the TableData interface
 * $URL: http://gwtsrwc.googlecode.com/svn/trunk/src/com/google/code/gwtsrwc/client/TableModel.java $
 */
public class TableModel<E extends TableData> {

	private List<ColumnDescriptor> columns;
	private List<E> rows;
	private List<TableModelListener> listeners;
	
	/**
	 * Create a TableModel based on the ColumnDescriptions given
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model; the order of the elements in the array will be used for the order of the columns in the display
	 */
	public TableModel(ColumnDescriptor[] desc) {
		columns = new ArrayList<ColumnDescriptor>();
		for(ColumnDescriptor c : desc)
			columns.add(c);
		rows = new ArrayList<E>();
		listeners = new ArrayList<TableModelListener>();
	}

	/**
	 * Create a TableModel based on the ColumnDescriptions given and fill the model with the given array of TableData objects
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model; the order of the elements in the array will be used for the order of the columns in the display
	 * @param data An array of objects who all implement the TableData interface; these objects will be used to populate the model; the order of the elements defines the order the objects will be displayed in (each object represents a row of the table)
	 */
	public TableModel(ColumnDescriptor[] desc, E[] data) {
		this(desc);
		for(E row : data)
			rows.add(row);
	}

	/**
	 * Create a TableModel based on the ColumnDescriptions given and fill the model with the given List of TableData objects
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model; the order of the elements in the array will be used for the order of the columns in the display
	 * @param data A List of objects who all implement the TableData interface; these objects will be used to populate the model; the order of the elements defines the order the objects will be displayed in (each object represents a row of the table)
	 */
	public TableModel(ColumnDescriptor[] desc, List<E> data) {
		this(desc);
		rows = data;
	}

	/**
	 * Get the current row count
	 * @return The current row count
	 */
	public int getRowCount() {
		return rows.size();
	}
	
	/**
	 * Get the current column count
	 * @return The column count
	 */
	public int getColumnCount() {
		return columns.size();
	}
	
	/**
	 * Return the ith row of the table
	 * @param i The row offset to grab
	 * @return The ith row of the table
	 * @throws IndexOutOfBoundsException if the specified offset does not exist in the table
	 */
	public E getRow(int i) {
		return rows.get(i);
	}
	
	/**
	 * Return all of the rows in the table
	 * @return A List containing all of the rows in the table
	 */
	public List<E> getRows() {
		return rows;
	}
	
	/**
	 * Retrieve the Widget at the specified cell in the table
	 * @param row The row offset to access
	 * @param col The column offset to access
	 * @return The Widget stored at the given location in the table
	 * @throws IndexOutOfBoundsException if the specified cell does not exist in the table
	 */
	public Widget getCell(int row, int col) {
		return rows.get(row).get(columns.get(col).getId());
	}

	/**
	 * Store the specified Widget at the specified cell in the table
	 * @param w The Widget to store at the specified cell
	 * @param row The row offset to insert into
	 * @param col The column offset to insert into
	 * @throws IndexOutOfBoundsException if the specified cell does not exist in the table
	 */
	public void setCell(Widget w, int row, int col) {
		rows.get(row).set(columns.get(col).getId(), w);
		fireCellUpdate(row, col);
		return;
	}
	
	/**
	 * Add a new row to the table
	 * @param data The data to be added
	 * @param row The location offset to insert the row into; any rows following this one will be pushed down in the table
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void addRow(E data, int row) {
		validateRow(data);
		rows.add(row, data);
		fireRowUpdate(row);
		return;
	}
	
	/**
	 * Add a new row to the end of the table
	 * @param data The data to be added
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void addRow(E data) {
		validateRow(data);
		rows.add(data);
		fireRowUpdate(rows.size() - 1);
		return;
	}
	
	/**
	 * Replace a row with the given data
	 * @param data The replacement data
	 * @param row The row offset to replace in the table
	 * @throws IndexOutOfBoundsException if the given row does not exist in the table
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void replaceRow(E data, int row) {
		validateRow(data);
		rows.remove(row);
		rows.add(row, data);
		fireRowUpdate(row);
		return;
	}
	
	// Ensure each row added to the model contains all of the columns described at model instantiation
	private void validateRow(E data) {
		for(ColumnDescriptor c : columns)
			if(data.get(c.getId()) == null)
				throw new IllegalArgumentException("Argument does not contain required column data: " + c.getId());
	}
	
	/**
	 * Get an array of all the Widgets stored in the given column
	 * @param col The column offset to grab
	 * @return An array of all the Widgets stored in the given column, ordered by the current ordering of the model at the time of the call
	 * @throws IndexOutOfBoundsException if the given column does not exist in the table
	 */
	public Widget[] getColumn(int col) {
		List<Widget> list = new ArrayList<Widget>();
		for(int i = 0; i < rows.size(); ++i)
			list.add(getCell(i, col));
		return list.toArray(new Widget[0]);
	}
	
	/**
	 * Sort the model on the given column
	 * @param column The column offset to sort on
	 * @param ascending True if the sort should be ascending or false for a descending sort
	 * @return True if the sort completed successfully or false otherwise; may fail if the given column does not exist or the column was marked as not sortable in the ColumnDescriptor
	 */
	public boolean sort(int column, boolean ascending) {
		ColumnDescriptor desc = columns.get(column);
		if(desc == null || !desc.getIsSortable())
			return false;

		boolean order = ascending ? true : false;
		// TODO Bubble sort is evil and definitely won't scale to large data sets (but serves my current needs)!!
		boolean swapped;
		do {
			swapped = false;
			for(int i = 1; i < rows.size(); ++i) {
				if(rows.get(i - 1).compare(rows.get(i), columns.get(column).getId()) == order) {
					E data = rows.remove(i);
					rows.add(i - 1, data);
					swapped = true;
				}
			}
		}while(swapped);
		fireSortedEvent();		
		return true;
	}

	/**
	 * Add a listener to this model
	 * @param l The listener object
	 */
	public void addListener(TableModelListener l) {
		listeners.add(l);
		return;
	}

	/**
	 * Return the column descriptor for the given column offset
	 * @param colOffset The column offset to retrieve information about
	 * @return The ColumnDescriptor for the given column offset
	 * @throws IndexOutOfBoundsException if the specified column offset does not exist
	 */
	public ColumnDescriptor getColumnDescriptor(int colOffset) {
		return columns.get(colOffset);
	}
	
	/**
	 * Remove the given listener from this model
	 * @param l The listener to remove
	 * @return True if the listener was removed or false otherwise; will return false if the given listener was not attached to this model
	 */
	public boolean removeListener(TableModelListener l) {
		return listeners.remove(l);
	}
	
	/**
	 * Remove all the listeners from this model
	 */
	public void removeAllListeners() {
		listeners.clear();
		return;
	}
	
	/**
	 * Fire the 'modelRowUpdated' event for the given row offset on this model
	 * @param row The row to be signaled as updated
	 */
	public void fireRowUpdate(int row) {
		for(TableModelListener l : listeners)
			l.modelRowUpdated(this, row);
		return;
	}
	
	/**
	 * Fire the 'modelCellUpdated' event for the given cell on this model
	 * @param row The row offset to be signaled as updated
	 * @param col The column offset to be signaled as updated
	 */
	public void fireCellUpdate(int row, int col) {
		for(TableModelListener l : listeners)
			l.modelCellUpdated(this, row, col);
		return;
	}
	
	/**
	 * Fire the 'modelSorted' event for this model
	 */
	protected void fireSortedEvent() {
		for(TableModelListener l : listeners)
			l.modelSorted(this);
		return;
	}
}